/*****************************************************************************
 * jQuery User Input - A new take on form validation with jQuery
 * 
 * @date		$Date: 2009-06-03 09:32:44 +0000 (Wed, 03 Jun 2009) $
 * @revision 	$Rev: 3 $
 * @copy		(c) Copyright 2009 Nicolas Rudas. All Rights Reserved.
 * @licence		MIT Licensed
 * @url			http://code.google.com/p/jqueryuserinput/
 * 
 *****************************************************************************/

(function() {
	var user_input;
	jQuery.extend({
		user_input: function(type,name,params) {
			var rules = user_input._rules,
				props = user_input._props;
			
			params =  ( (name && name.constructor !== Array && typeof name === 'object' )
						|| typeof name === 'function')
							? name : params; 
			name = (typeof name === 'string' || name && name.constructor === Array)
					? name : null ;
			params = (typeof params === 'object' || typeof params === 'function') ? params : null;
		
		// Set/Get new rule	
			if(type === 'rule' && typeof name === 'string') {
				if(name && params) { rules[name] = params; }
				else if(name && !params) { return rules[name];  } }
		//	Set/Get properties
			else if(type && name) {
				var prop_type = user_input._props[type] = user_input._props[type] || {};
				if(params) { 
					name = $.makeArray(name);
					$.each(name,function(i,n) {
						prop_type[n] = params;
					});
				}
				else if(!params && typeof name === 'string') {
					return prop_type[name];  }	}
		// Something else (remove)
			else {
				console.error('WTF?',type,name,params);
			}
		},
		check_user_input: function(el,event){
			el = (!el.jquery) ? $(el) : el;
			console.time('Check');
			var	_tmp,
				errors = [],
				rules_groups = user_input._input_selectors,
				rules_elems = user_input._storage_elems;
			
			el.each(function(elem_index){
				var $this = $(this),
					checked_rules = [],
					check_in_groups = check_this_element($this);
					
				if(!check_in_groups) {
				 	return; }// no applicable rule for this element
				
				$.each(check_in_groups,function(selector,selector_group) {
					var elements_for_this_group = el.filter(selector),
						rules_per_event = (event && selector_group[event]);

				 // Check for specific event
					if (rules_per_event) {
						$.each(rules_per_event,function(i,rule) {
							
							var value_is_ok = checkValue(rule,$this,elements_for_this_group);
							
							if(value_is_ok !== true){
								errors.push(rule);	}
						
						});
					}
					
 				// Check all events
					else if(!event) {
						$.each(selector_group,function(i,rule_array) {

							$.each(rule_array,function(k,rule) {
								if($.inArray(rule.__id,checked_rules) !== -1 ){
									return;} // Rule already checked
									
								var value_is_ok = checkValue(rule,$this,elements_for_this_group);

								if(value_is_ok !== true){
									errors.push(rule); }
								
								checked_rules.push(rule.__id);
							});
						});
					}
				});
			});
			
			console.timeEnd('Check');
			return !Boolean(errors.length);	

			function check_this_element(el) {
				var r = {};
				for(var group in rules_groups) {
					if( el.is(group) ) {r[group] = rules_groups[group];}};
				return r;
			};
		}
	});

/**
 * $(selector).user_input([rule_name],rules)
 * Set up user input check for each element
 * @ param	rule_name (optional)
 *			{String}	The name of the rule to apply for checking (rule must be defined beforehand)
 * @ param	rules
 *			{Object}	The rules to apply for checking
 *						If rule_name provided, rules object extends existing rules
**/

	jQuery.fn.user_input = function(name,params) { // set up user input
		params = (!params && typeof name === 'object') ? name : params;
		name = (typeof name === 'string') ? name : null ;
		
		var self = this,
			events,
			add_params = params || {},
			add_rule = (name && user_input._rules[name]) || {},
			new_rule = $.extend({},add_rule,add_params);
		
		new_rule.__id = (function() {return parseInt((new Date().valueOf()+Math.floor(Math.random()*new Date().valueOf()))/(Math.floor(Math.random()*9)+1),10);})();
		
		if(new_rule.event) { // bind each new event
			events = $.makeArray(new_rule.event);
			
			$.each(events,function(i,evt) {
				var e = get_property('event',evt,Function);
				(e && e.apply(self,[self,evt]));
			});
			
			delete new_rule.event;
		}
		else {
			throw new Error('No event??');
		}
		
		var group_id = this.selector,
			rules_group = user_input._input_selectors[group_id] = user_input._input_selectors[group_id] || {};
		
		$.each(events,function(i,evt) {
			var evt_arr = rules_group[evt] = rules_group[evt] || [];
			evt_arr.push(new_rule); 
		});
		
		return this;
	};
	
	
/**
 * $(selector).check_user_input([event_name])
 *	Checks each element for the specified event
 * 	If no event specified, check for all events (slower)
 * 	!! NOT CHAINABLE !!
 *  @ param		event (optional)
 *				{Object}	jQuery event Object; OR
 *				{String}	event name/type
 *	@ return	true	(no errors)
 *				false	(errors)
 **/	
	jQuery.fn.check_user_input = function(evt) { // check user input
		var r, $this = $(this);
			
		evt = (evt && evt.type)
				? evt.type : (evt && typeof evt === 'string' || evt.constructor === Array)
								? evt : null;
		if(evt) {
			evt = $.makeArray(evt);
			$.each(evt,function(i,e) { // check for single or multiple events
				return (r = $.check_user_input($this,e));
			});	}
			
		else { // check for all events
			r = $.check_user_input(this);}
		return r;
	};
	

// Helper functions	
	function checkValue(rule,el,elems) { // All the good stuff is here
		var _tmp,
			check_value = get_property('value',rule.value);
			existing_el_errors = el.data('user_input_errors') || [];

		if(!rule.value || !check_value){
			new Error('No value check defined'); return false; }
	
	// Call before function
	// Only for first element
		if(elems[ 0 ] == el[0]) {
			_tmp = get_property('before',rule.before,Function);
			( _tmp && _tmp.apply(elems,[get_elem_errors(elems,rule)]) );
		}
	
	// Check value
		if(typeof check_value === 'function') {
			check_value = check_value.apply(elems,[el,el.val(),rule]); }
			
		else if(check_value.constructor === RegExp) {
			check_value = Boolean(el.val().match(check_value)); }
	
	// Value is not OK. Call error callback
		if(check_value !== true) {
			(function() {
		// If error already registered, ignore
		// This makes the error callback to behave like an on/off switch
		// i.e. If its on can't turn it on again.
			for (var i = existing_el_errors.length - 1; i >= 0; i--){
				if( existing_el_errors[i].rule == rule.__id ){
					return;	} };
			
		// Save a reference to returned value (perhaps jQuery Elements)
		// so that the cleanup function can remove them
			_tmp = get_property('error',rule.error,Function);
			var error_callback_said = (_tmp && _tmp.apply(elems,[el,rule]));
			existing_el_errors.push({
				rule: rule.__id,
				cleanup: error_callback_said });
			
			el.data('user_input_errors',existing_el_errors);
			})();
		}
	// Value is OK.
	// Clean up possible error messages (cleanup callback),
	// change value to new_value, and call success
		else if(check_value === true ) {
		// Call cleanup function
			_tmp = get_property('cleanup',rule.cleanup,Function);
			$.each(existing_el_errors,function(i,error) {
				if(!error || error.rule != rule.__id){
					return;}// only clean up errors for the current rule
				(_tmp && _tmp.apply(error.cleanup,[el,rule]));
				(error.cleanup && error.cleanup.jquery && error.cleanup.length// make sure to delete references 
					&& delete error.cleanup); //to DOM elements stored in error
				existing_el_errors.splice(i,1); // remove error from list
			});

		// Call new value function
			_tmp = get_property('new_value',rule.new_value,Function);
			(_tmp && (el.val(_tmp.apply(elems,[el,el.val(),rule]))));
			
		// Call success function
			_tmp = get_property('success',rule.success,Function);
			( _tmp && _tmp.apply(elems,[el,rule]) );
		}

	// Call complete function
	// Only for last element
		if(elems[ elems.length - 1 ] == el[0]) {
			_tmp = get_property('complete',rule.complete,Function);
			( _tmp && _tmp.apply(elems,[get_elem_errors(elems,rule)]) ); }	
		
		return check_value;
	};
	
	function get_elem_errors(elems,rule) {
		var r = {elems: [],	msgs: []};
		
		elems.each(function() {
			var _this = this,
				$this = $(this),
				elem_errors = $this.data('user_input_errors');
				
			if(!elem_errors){return;}

			$.each(elem_errors,function(i,er) {
				if(er.rule == rule.__id) {
					r.elems.push(_this);
					r.msgs.push(er.cleanup);
				}
			});
		});
		
		r.elems = $(r.elems);
		return r;
	};
	
	function get_property(type,name,constr) {
		var r;
		
		if(name && name.constructor === constr) {return name;}
		else if ( !name ) { return; }
		
		r = (typeof name === 'string' && typeof type === 'string')
				? user_input._props[type][name]
				: name;
				
		if(!r){return null;}
		
		constr = constr || r.constructor;
		return  (r.constructor === constr) ? r : null ;
	};

	$.user_input._rules = {};
	$.user_input._props = {};
	$.user_input._input_selectors = {};
	user_input = $.user_input;	
})();

// ===========================
// = Common Validation Rules =
// ===========================
// =============
// = Rule Sets =
// =============
$.user_input('rule','default',{
	error: function(elem,rule) {
		var parent = elem.parents(rule.parent).addClass('ui-state-highlight');
		return	$('<span class="ui-state-error">'+rule.message+'</span>')
			.appendTo(parent);
	},
	success: function(elem,rule) {
		var parent = elem.parents(rule.parent).removeClass('ui-state-highlight');
	},
	event: ['change','submit'],
	cleanup: 'remove'
});

// ==========
// = Events =
// ==========
// this in function refers to all elements in selection (>= 1)
// param el is current element (= 1)
// param event is the event being attached
$.user_input('event','blur',function(el) {
	el.unbind('blur.user_input').bind('blur.user_input',function(e,event) {
		return $(this).check_user_input(e);
	});
});

$.user_input('event',['keyup','keypress','keydown'],function(el,event) {
	var validCodes = [ 
		8, // backspace
		46, // delete
		32, // space
		13 // return
	];
	el.unbind(event+'.user_input').bind(event+'.user_input',function(e) {
		var $this = $(this),
			keyCode = e.keyCode,
			which = String.fromCharCode(keyCode);
		
	// Ignore events that don't change the value
		if( !which.match(/.*?/) && $.inArray(keyCode,validCodes) == -1 ) {
			return;}

		if( $this.data('type_user_input') ){
			clearTimeout($this.data('type_user_input') ); }
	
		$this.data('type_user_input',setTimeout(function() {
			clearTimeout($this.data('type_user_input'));
			$this.data('type_user_input',false);
			$this.check_user_input(e);
		},300));
	});
});


$.user_input('event','submit',function(el) {
	var self = this,
		form = el.parents('form:first'),
		form_elements = form.data('user_input_elements') || [] ;
// Only check for each element once	
	el.each(function() {
		if($.inArray(this,form_elements) === -1) {form_elements.push(this);}});	
	form
		.data('user_input_elements',form_elements)
		.unbind('submit.user_input')
		.bind('submit.user_input', function(e,data) {
			var elems = $(this).data('user_input_elements');elems = $(elems);
			return elems.check_user_input(e);
		});
});

// =============
// = Callbacks =
// =============
$.user_input('cleanup',['fadeOut','slideUp','remove'],function(elem,rule){
	if(rule.cleanup === 'remove' && this.remove){this.remove();return;}
	this[rule.cleanup] && this[rule.cleanup](function() { $(this).remove(); });
});
// ==========
// = Values =
// ==========
// this in function refers to all elements in selection (>= 1)
// param elem is current element (= 1)
// param rule is the rule object being checked
$.user_input('value','required',function(elem,value,rule) {
	return value && value.length !== 0;
});

// From jQuery validation plug-in 1.5.2
$.user_input('value','email',function(elem,value,rule) {
	return (!value && !value.length) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value);
});

// From jQuery validation plug-in 1.5.2
$.user_input('value','url',function(elem,value,rule) {
	return (!value && !value.length) || /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value);
});